package mybatis.executor;

import mybatis.exception.ExecutorException;
import mybatis.exception.MyBatisException;
import mybatis.mapping.BoundSql;
import mybatis.mapping.MappedStatement;
import mybatis.mapping.ParameterMapping;
import mybatis.session.Configuration;
import mybatis.transaction.Transaction;
import utils.LogUtils;
import utils.ReflectUtils;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.*;
import java.util.*;

/**
 * 通用简单查询执行器实现类
 *
 * @author richie696
 * @version 1.0
 * @since 2020/11/07
 */
public class SimpleExecutor extends BaseExecutor {

    public SimpleExecutor(Configuration configuration, Transaction transaction) {
        super(configuration, transaction);
    }

    @Override
    protected int doUpdate(MappedStatement ms, BoundSql boundSql, Object parameter) throws SQLException {
        PreparedStatement pStatement = null;
        ResultSet resultSet = null;
        try {
            Connection connection = getConnection();
            if (connection.isReadOnly()) {
                return 0;
            }
            // 这里要判断是否是执行insert操作，如果是的话需要设置RETURN_GENERATED_KEYS
            if (ms.getSqlCommandType().isInsert()) {
                pStatement = connection.prepareStatement(boundSql.getSql(), Statement.RETURN_GENERATED_KEYS);
            } else {
                pStatement = connection.prepareStatement(boundSql.getSql());
            }
            String parameterType = ms.getParameterType();
            if (Objects.isNull(parameterType)) {
                parameterType = parameter.getClass().getName();
            }
            if (configuration.containDefaultClass(parameterType)) {
                setBasicParameterValue(parameter, pStatement);
            } else {
                setObjectParameterValue(boundSql, pStatement, parameter);
            }
            int result = pStatement.executeUpdate();
            if (ms.getSqlCommandType().isInsert() && ms.isUseGeneratedKeys()) {
                resultSet = pStatement.getGeneratedKeys();
                try {
                    setGeneratedKey(ms, resultSet, parameter);
                } catch (IntrospectionException | InvocationTargetException | IllegalAccessException e) {
                    throw new ExecutorException(e);
                }
            }
            return result;
        } catch (Exception ex) {
            throw new ExecutorException(ex);
        } finally {
            releaseResource(pStatement, resultSet);
        }
    }

    /**
     * 执行真实查询操作的方法
     *
     * @param ms         SQL映射关系对象
     * @param boundSql   SQL语句对象
     * @param parameters 查询参数
     * @param <E>        查询结果泛型对象
     * @return 返回符合查询条件的结果对象
     * @throws SQLException 当查询过程中产生数据库相关的错误时抛出该异常
     */
    @Override
    protected <E> List<E> doQuery(MappedStatement ms, BoundSql boundSql, Object... parameters) throws SQLException {
        Connection connection;
        PreparedStatement pStatement = null;
        ResultSet resultSet = null;
        try {
            connection = getConnection();
            pStatement = connection.prepareStatement(boundSql.getSql());
            String parameterType = ms.getParameterType();
            if (configuration.containDefaultClass(parameterType)) {
                setBasicParameterValue(parameters[0], pStatement);
            } else {
                setObjectParameterValue(boundSql, pStatement, parameters);
            }
            resultSet = pStatement.executeQuery();
            return convertResultSet(resultSet, ms.getResultTypeClass());
        } catch (Exception ex) {
            LogUtils.error(ex.getMessage(), ex);
            throw new MyBatisException(ex.getMessage(), ex);
        } finally {
            releaseResource(pStatement, resultSet);
        }
    }

    private void setGeneratedKey(MappedStatement ms, ResultSet resultSet, Object parameter)
            throws IntrospectionException, SQLException, InvocationTargetException, IllegalAccessException {
        String idKey = ms.getKeyProperty();
        PropertyDescriptor descriptor = new PropertyDescriptor(idKey, parameter.getClass());
        Method setter = descriptor.getWriteMethod();
        if (!resultSet.next()) {
            throw new ExecutorException("主键获取失败");
        }
        long id = resultSet.getLong(1);
        setter.invoke(parameter, id);
    }

    /**
     * 设置Java基本类型参数值的方法
     *
     * @param parameter  参数值
     * @param pStatement 查询语句的statement对象
     * @throws SQLException 数据库连接出现问题时抛出该异常
     */
    private void setBasicParameterValue(Object parameter, PreparedStatement pStatement) throws SQLException {
        if (Objects.isNull(parameter)) {
            return;
        }
        pStatement.setObject(1, parameter);
    }

    /**
     * 设置自定义类型参数值的方法
     *
     * @param boundSql   SQL语句对象
     * @param pStatement 查询语句的statement对象
     * @param parameters 查询的参数列表
     * @throws IllegalAccessException    反射获取对象参数值时，如果参数不存在或无权限访问的时候抛出该异常
     * @throws InvocationTargetException 反射获取对象参数值时，如果对应Getter方法无法调用的时候抛出该异常
     * @throws SQLException              数据库连接出现问题时抛出该异常
     * @throws NoSuchMethodException     找不到需要的属性 setter 方法时抛出该异常
     */
    private void setObjectParameterValue(BoundSql boundSql, PreparedStatement pStatement, Object... parameters)
            throws IllegalAccessException, InvocationTargetException, SQLException, NoSuchMethodException {
        if (Objects.isNull(parameters) || parameters.length == 0) {
            return;
        }
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        int size = parameterMappings.size();
        for (int i = 0; i < size; i++) {
            String content = parameterMappings.get(i).getContent();
            Object value = ReflectUtils.getParameterValue(parameters[0], new StringBuilder(content));
            pStatement.setObject(i + 1, value);
        }
    }

    /**
     * 转换数据库查询结果集对象的方法
     *
     * @param resultSet  结果集对象
     * @param resultType 转换的结果记录对象类型
     * @param <E>        结果记录对象类型
     * @return 返回查询到的结果列表
     */
    @SuppressWarnings("unchecked")
    private <E> List<E> convertResultSet(ResultSet resultSet, Class<?> resultType) {
        List<E> resultList;
        try {
            ResultSetMetaData metaData = resultSet.getMetaData();
            int count = metaData.getColumnCount();
            resultList = new ArrayList<>(count);
            while (resultSet.next()) {
                Object recordInstance = resultType.getDeclaredConstructor().newInstance();
                for (int i = 1; i <= count; i++) {
                    String columnName = metaData.getColumnName(i);
                    Object value = resultSet.getObject(columnName);
                    columnName = getFieldName(columnName);
                    PropertyDescriptor descriptor = new PropertyDescriptor(columnName, resultType);
                    Method setter = descriptor.getWriteMethod();
                    Class<?> paramType = setter.getParameterTypes()[0];
                    setValue(setter, recordInstance, paramType, value);
                }
                resultList.add((E) recordInstance);
            }
        } catch (Exception e) {
            LogUtils.error(e.getMessage(), e);
            resultList = Collections.emptyList();
        }
        return resultList;
    }

    private void setValue(Method setter, Object recordInstance, Class<?> paramType, Object paramValue)
            throws InvocationTargetException, IllegalAccessException {
        if (paramType.equals(BigInteger.class)) {
            setter.invoke(recordInstance, BigInteger.valueOf((long) paramValue));
        } else if (paramType.equals(BigDecimal.class)) {
            setter.invoke(recordInstance, BigDecimal.valueOf((long) paramValue));
        } else {
            setter.invoke(recordInstance, paramValue);
        }
    }

    /**
     * 将异形数据库列名称按照规则转换为Java类的字段名的方法
     *
     * @param columnName 数据库字段名
     * @return 返回Java类字段名
     */
    private String getFieldName(String columnName) {
        StringTokenizer tokenizer = new StringTokenizer(columnName.toLowerCase(), "_");
        StringBuilder className = new StringBuilder();
        String name;
        int i = 0;
        while (tokenizer.hasMoreTokens()) {
            name = tokenizer.nextToken();
            if (i++ == 0) {
                className.append(name);
                continue;
            }
            className.append(name.substring(0, 1).toUpperCase()).append(name.substring(1));
        }
        return className.toString();
    }

    private void releaseResource(PreparedStatement pStatement, ResultSet resultSet) throws SQLException {
        if (resultSet != null) {
            resultSet.close();
        }
        if (pStatement != null) {
            pStatement.close();
        }
    }

}
